---
title: Post-Event Contact Export
description: Run overnight contact extraction from LinkedIn, X, or other social platforms after networking events
---

import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

## Overview

After networking events, you need to export new connections from LinkedIn, X, or other platforms into your CRM. This automation handles it for you.

**The workflow**: Kick off the script after an event and let it run overnight. Wake up to a clean CSV ready for your CRM or email tool.

This example focuses on LinkedIn but works across platforms. It uses [Cua Computer](/computer-sdk/computers) to interact with web interfaces and [Agent Loops](/agent-sdk/agent-loops) to iterate through connections with conversation history.

### Why Cua is Perfect for This

**Cua's VMs save your session data**, bypassing bot detection entirely:

- **Log in once manually** through the VM browser
- **Session persists** - you appear as a regular user, not a bot
- **No captchas** - the platform treats automation like normal browsing
- **No login code** - script doesn't handle authentication
- **Run overnight** - kick off and forget

Traditional web scraping triggers anti-bot measures immediately. Cua's approach works across all platforms.

### What You Get

The script generates two files with your extracted connections:

**CSV Export** (`linkedin_connections_20250116_143022.csv`):

```csv
first,last,role,company,met_at,linkedin
John,Smith,Software Engineer,Acme Corp,Google Devfest Toronto,https://www.linkedin.com/in/johnsmith
Sarah,Johnson,Product Manager,Tech Inc,Google Devfest Toronto,https://www.linkedin.com/in/sarahjohnson
```

**Messaging Links** (`linkedin_messaging_links_20250116_143022.txt`):

```
LinkedIn Messaging Compose Links
================================================================================

1. https://www.linkedin.com/messaging/compose/?recipient=johnsmith
2. https://www.linkedin.com/messaging/compose/?recipient=sarahjohnson
```

---

<Steps>

<Step>

### Set Up Your Environment

First, install the required dependencies:

Create a `requirements.txt` file:

```text
cua-agent
cua-computer
python-dotenv>=1.0.0
```

Install the dependencies:

```bash
pip install -r requirements.txt
```

Create a `.env` file with your API keys:

```text
ANTHROPIC_API_KEY=your-anthropic-api-key
CUA_API_KEY=sk_cua-api01...
CUA_CONTAINER_NAME=m-linux-...
```

</Step>

<Step>

### Log Into LinkedIn Manually

**Important**: Before running the script, manually log into LinkedIn through your VM:

1. Access your VM through the Cua dashboard
2. Open a browser and navigate to LinkedIn
3. Log in with your credentials (handle any captchas manually)
4. Close the browser but leave the VM running
5. Your session is now saved and ready for automation!

This one-time manual login bypasses all bot detection.

</Step>

<Step>

### Configure and Create Your Script

Create a Python file (e.g., `contact_export.py`). You can customize:

```python
# Where you met these connections (automatically added to CSV)
MET_AT_REASON = "Google Devfest Toronto"

# Number of contacts to extract (in the main loop)
for contact_num in range(1, 21):  # Change 21 to extract more/fewer contacts
```

Select your environment:

<Tabs items={['Cloud Sandbox', 'Linux on Docker', 'macOS Sandbox', 'Windows Sandbox']}>
  <Tab value="Cloud Sandbox">

```python
import asyncio
import csv
import logging
import os
import signal
import traceback
from datetime import datetime

from agent import ComputerAgent
from computer import Computer, VMProviderType
from dotenv import load_dotenv

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Configuration: Define where you met these connections
MET_AT_REASON = "Google Devfest Toronto"

def handle_sigint(sig, frame):
    print("\n\nExecution interrupted by user. Exiting gracefully...")
    exit(0)

def extract_public_id_from_linkedin_url(linkedin_url):
    """Extract public ID from LinkedIn profile URL."""
    if not linkedin_url:
        return None

    url = linkedin_url.split('?')[0].rstrip('/')

    if '/in/' in url:
        public_id = url.split('/in/')[-1]
        return public_id

    return None

def extract_contact_from_response(result_output):
    """
    Extract contact information from agent's response.
    Expects format:
    FIRST: value
    LAST: value
    ROLE: value
    COMPANY: value
    LINKEDIN: value
    """
    contact = {
        'first': '',
        'last': '',
        'role': '',
        'company': '',
        'met_at': MET_AT_REASON,
        'linkedin': ''
    }

    for item in result_output:
        if item.get("type") == "message":
            content = item.get("content", [])
            for content_part in content:
                text = content_part.get("text", "")
                if text:
                    for line in text.split('\n'):
                        line = line.strip()
                        line_upper = line.upper()

                        if line_upper.startswith("FIRST:"):
                            value = line[6:].strip()
                            if value and value.upper() != "N/A":
                                contact['first'] = value
                        elif line_upper.startswith("LAST:"):
                            value = line[5:].strip()
                            if value and value.upper() != "N/A":
                                contact['last'] = value
                        elif line_upper.startswith("ROLE:"):
                            value = line[5:].strip()
                            if value and value.upper() != "N/A":
                                contact['role'] = value
                        elif line_upper.startswith("COMPANY:"):
                            value = line[8:].strip()
                            if value and value.upper() != "N/A":
                                contact['company'] = value
                        elif line_upper.startswith("LINKEDIN:"):
                            value = line[9:].strip()
                            if value and value.upper() != "N/A":
                                contact['linkedin'] = value

    return contact

async def scrape_linkedin_connections():
    """Scrape LinkedIn connections and export to CSV."""

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    csv_filename = f"linkedin_connections_{timestamp}.csv"
    csv_path = os.path.join(os.getcwd(), csv_filename)

    # Initialize CSV file
    with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
        writer.writeheader()

    print(f"\n🚀 Starting LinkedIn connections scraper")
    print(f"📁 Output file: {csv_path}")
    print(f"📍 Met at: {MET_AT_REASON}")
    print("=" * 80)

    try:
        async with Computer(
            os_type="linux",
            provider_type=VMProviderType.CLOUD,
            name=os.environ["CUA_CONTAINER_NAME"],  # Your sandbox name
            api_key=os.environ["CUA_API_KEY"],
            verbosity=logging.INFO,
        ) as computer:

            agent = ComputerAgent(
                model="cua/anthropic/claude-sonnet-4.5",
                tools=[computer],
                only_n_most_recent_images=3,
                verbosity=logging.INFO,
                trajectory_dir="trajectories",
                use_prompt_caching=True,
                max_trajectory_budget=10.0,
            )

            history = []

            # Task 1: Navigate to LinkedIn connections page
            navigation_task = (
                "STEP 1 - NAVIGATE TO LINKEDIN CONNECTIONS PAGE:\n"
                "1. Open a web browser (Chrome or Firefox)\n"
                "2. Navigate to https://www.linkedin.com/mynetwork/invite-connect/connections/\n"
                "3. Wait for the page to fully load\n"
                "4. Confirm you can see the list of connections\n"
                "5. Ready to start extracting contacts"
            )

            print(f"\n[Task 1/21] Navigating to LinkedIn...")
            history.append({"role": "user", "content": navigation_task})

            async for result in agent.run(history, stream=False):
                history += result.get("output", [])

            print(f"✅ Navigation completed\n")

            # Extract 20 contacts
            contacts_extracted = 0
            linkedin_urls = []
            previous_contact_name = None

            for contact_num in range(1, 21):
                # Build extraction task
                if contact_num == 1:
                    extraction_task = (
                        f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\n"
                        f"1. Click on the first connection's profile\n"
                        f"2. Extract: FIRST, LAST, ROLE, COMPANY, LINKEDIN URL\n"
                        f"3. Return in exact format:\n"
                        f"FIRST: [value]\n"
                        f"LAST: [value]\n"
                        f"ROLE: [value]\n"
                        f"COMPANY: [value]\n"
                        f"LINKEDIN: [value]\n"
                        f"4. Navigate back to connections list"
                    )
                else:
                    extraction_task = (
                        f"STEP {contact_num + 1} - EXTRACT CONTACT {contact_num} OF 20:\n"
                        f"1. Find '{previous_contact_name}' in the list\n"
                        f"2. Click on the contact BELOW them\n"
                        f"3. Extract: FIRST, LAST, ROLE, COMPANY, LINKEDIN URL\n"
                        f"4. Return in exact format:\n"
                        f"FIRST: [value]\n"
                        f"LAST: [value]\n"
                        f"ROLE: [value]\n"
                        f"COMPANY: [value]\n"
                        f"LINKEDIN: [value]\n"
                        f"5. Navigate back"
                    )

                print(f"[Task {contact_num + 1}/21] Extracting contact {contact_num}/20...")
                history.append({"role": "user", "content": extraction_task})

                all_output = []
                async for result in agent.run(history, stream=False):
                    output = result.get("output", [])
                    history += output
                    all_output.extend(output)

                contact_data = extract_contact_from_response(all_output)

                has_name = bool(contact_data['first'] and contact_data['last'])
                has_linkedin = bool(contact_data['linkedin'] and 'linkedin.com' in contact_data['linkedin'])

                if has_name or has_linkedin:
                    with open(csv_path, 'a', newline='', encoding='utf-8') as csvfile:
                        writer = csv.DictWriter(csvfile, fieldnames=['first', 'last', 'role', 'company', 'met_at', 'linkedin'])
                        writer.writerow(contact_data)
                    contacts_extracted += 1

                    if contact_data['linkedin']:
                        linkedin_urls.append(contact_data['linkedin'])

                    if has_name:
                        previous_contact_name = f"{contact_data['first']} {contact_data['last']}".strip()

                    name_str = f"{contact_data['first']} {contact_data['last']}" if has_name else "[No name]"
                    print(f"✅ Contact {contact_num}/20 saved: {name_str}")
                else:
                    print(f"⚠️  Could not extract valid data for contact {contact_num}")

                if contact_num % 5 == 0:
                    print(f"\n📈 Progress: {contacts_extracted}/{contact_num} contacts extracted\n")

            # Create messaging links file
            messaging_filename = f"linkedin_messaging_links_{timestamp}.txt"
            messaging_path = os.path.join(os.getcwd(), messaging_filename)

            with open(messaging_path, 'w', encoding='utf-8') as txtfile:
                txtfile.write("LinkedIn Messaging Compose Links\n")
                txtfile.write("=" * 80 + "\n\n")

                for i, linkedin_url in enumerate(linkedin_urls, 1):
                    public_id = extract_public_id_from_linkedin_url(linkedin_url)
                    if public_id:
                        messaging_url = f"https://www.linkedin.com/messaging/compose/?recipient={public_id}"
                        txtfile.write(f"{i}. {messaging_url}\n")

            print("\n" + "="*80)
            print("🎉 All tasks completed!")
            print(f"📁 CSV file saved to: {csv_path}")
            print(f"📊 Total contacts extracted: {contacts_extracted}/20")
            print(f"💬 Messaging links saved to: {messaging_path}")
            print("="*80)

    except Exception as e:
        print(f"\n❌ Error: {e}")
        traceback.print_exc()
        raise

def main():
    try:
        load_dotenv()

        if "ANTHROPIC_API_KEY" not in os.environ:
            raise RuntimeError("Please set ANTHROPIC_API_KEY in .env")

        if "CUA_API_KEY" not in os.environ:
            raise RuntimeError("Please set CUA_API_KEY in .env")

        if "CUA_CONTAINER_NAME" not in os.environ:
            raise RuntimeError("Please set CUA_CONTAINER_NAME in .env")

        signal.signal(signal.SIGINT, handle_sigint)

        asyncio.run(scrape_linkedin_connections())

    except Exception as e:
        print(f"\n❌ Error: {e}")
        traceback.print_exc()

if __name__ == "__main__":
    main()
```

  </Tab>
  <Tab value="Linux on Docker">

```python
# Same code as Cloud Sandbox, but change Computer initialization to:
async with Computer(
    os_type="linux",
    provider_type=VMProviderType.DOCKER,
    image="trycua/cua-xfce:latest",
    verbosity=logging.INFO,
) as computer:
```

And remove the `CUA_API_KEY` and `CUA_CONTAINER_NAME` requirements from `.env` and the validation checks.

  </Tab>
  <Tab value="macOS Sandbox">

```python
# Same code as Cloud Sandbox, but change Computer initialization to:
async with Computer(
    os_type="macos",
    provider_type=VMProviderType.LUME,
    name="macos-sequoia-cua:latest",
    verbosity=logging.INFO,
) as computer:
```

And remove the `CUA_API_KEY` and `CUA_CONTAINER_NAME` requirements from `.env` and the validation checks.

  </Tab>
  <Tab value="Windows Sandbox">

```python
# Same code as Cloud Sandbox, but change Computer initialization to:
async with Computer(
    os_type="windows",
    provider_type=VMProviderType.WINDOWS_SANDBOX,
    verbosity=logging.INFO,
) as computer:
```

And remove the `CUA_API_KEY` and `CUA_CONTAINER_NAME` requirements from `.env` and the validation checks.

  </Tab>
</Tabs>

</Step>

<Step>

### Run Your Script

Execute your contact extraction automation:

```bash
python contact_export.py
```

The agent will:

1. Navigate to your LinkedIn connections page
2. Extract data from 20 contacts (first name, last name, role, company, LinkedIn URL)
3. Save contacts to a timestamped CSV file
4. Generate messaging compose links for easy follow-up

Monitor the output to see the agent's progress. The script will show a progress update every 5 contacts.

</Step>

</Steps>

---

## How It Works

This script demonstrates a practical workflow for extracting LinkedIn connection data:

1. **Session Persistence** - Manually log into LinkedIn through the VM once, and the VM saves your session
2. **Navigation** - The script navigates to your connections page using your saved authenticated session
3. **Data Extraction** - For each contact, the agent clicks their profile, extracts data, and navigates back
4. **Python Processing** - Python parses responses, validates data, and writes to CSV incrementally
5. **Output Files** - Generates a CSV with contact data and a text file with messaging URLs

## Next Steps

- Learn more about [Cua computers](/computer-sdk/computers) and [computer commands](/computer-sdk/commands)
- Read about [Agent loops](/agent-sdk/agent-loops), [tools](/agent-sdk/custom-tools), and [supported model providers](/agent-sdk/supported-model-providers/)
- Experiment with different [Models and Providers](/agent-sdk/supported-model-providers/)
- Adapt this script for other platforms (Twitter/X, email extraction, etc.)
- Join our [Discord community](https://discord.com/invite/mVnXXpdE85) for help
